/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package com.googlecode.jihoonPlaygroundArena.db.sql.mysql.beans;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.AttributeOverride;
import javax.persistence.CascadeType;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.Lob;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Version;

import org.hibernate.annotations.CollectionId;
import org.hibernate.annotations.ForeignKey;

import com.googlecode.jihoonPlaygroundArena.shared.utils.JPAConstants;

/**
 * @org.hibernate.annotations.Entity(dynamicInsert and dynamicUpdate)
 *  should be only provided within large application where you do not wish Hibernate 
 *  to generate all SQL statements which could impact the memory footprint.
 * 
 * @author Ji Hoon Kim
 */
@Entity
@Table(name="ITEM")
@org.hibernate.annotations.Entity(
        dynamicInsert=true,
        dynamicUpdate=true
)
public class Item {
    
    private Long id;
    private String name;
    private User seller;
    private String description;
    private Double price;
    private Double shippingFee;
    private Double taxRate;
    private Date lastModified;
    private Set<Image> images = new HashSet<Image>();
    private Collection<Review> reviews = new ArrayList<Review>();
    private Set<Category> categories = new HashSet<Category>();
    private Set<Receipt> receipts = new HashSet<Receipt>();
    private Integer version;
    
    private BigDecimal totalIncludingTax;
    
    @Id
    @GeneratedValue
    @Column(name="ITEM_ID")
    public Long getId() {
        return id;
    }
    void setId(Long id) {
    	this.id = id;
    }
    
    @Column(name="NAME",
            nullable=false
    )
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    @ManyToOne
    @ForeignKey(name="FK_SELLER")
    /**
     * Foreign key constraints, while generated by Hibernate, have a fairly unreadable name. You can 
     * override the constraint name using @ForeignKey
     */
    @org.hibernate.annotations.Fetch(org.hibernate.annotations.FetchMode.SELECT)
    public User getSeller() {
        return seller;
    }
    public void setSeller(User seller) {
        this.seller = seller;
    }
    
    /**
     * Clob and Blob provides lazy loading without bytecode instrumentation.
     * 
     * When the owner of the property is loaded, the property value is a locator object--effectively, 
     * a pointer to the real value that isn't yet materialized. Once you access the property, the value 
     * is materialized. This on-demand loading works only as long as the database transaction is open, so 
     * you need to access any property of such a type when the owning entity instance is in a persistent 
     * and transactional state, not in detached state.
     */
    @Lob
    @Column(name="DESCRIPTION",
            nullable=false,
            length=1024
    )
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    
    @Column(name="PRICE",
            nullable=false
    )
    public Double getPrice() {
        return price;
    }
    public void setPrice(Double price) {
        this.price = price;
    }
    
    @Column(name="SHIPPING_FEE",
            nullable=false
    )
    public Double getShippingFee() {
        return shippingFee;
    }
    public void setShippingFee(Double shippingFee) {
        this.shippingFee = shippingFee;
    }
    
    @Column(name="TAX_RATE")
    public Double getTaxRate() {
        return taxRate;
    }
    public void setTaxRate(Double taxRate) {
        this.taxRate = taxRate;
    }
    
    @org.hibernate.annotations.Formula("TOTAL + TAX * TOTAL")
    public BigDecimal getTotalIncludingTax() {
        return totalIncludingTax;
    }
    void setTotalIncludingTax(BigDecimal totalIncludingTax) {
    	this.totalIncludingTax = totalIncludingTax;
    }
    
    @Column(name="LAST_MODIFIED",
            updatable=false,
            insertable=false
    )
    @org.hibernate.annotations.Generated(
            org.hibernate.annotations.GenerationTime.ALWAYS
    )  
    public Date getLastModified() {
        return lastModified;
    }
    void setLastModified(Date lastModified) {
    	this.lastModified = lastModified;
    }
    
    @ElementCollection
    @CollectionTable(name="ITEM_IMAGES", joinColumns={@JoinColumn(name="ITEM_ID")})
    @Column
    @org.hibernate.annotations.OrderBy(clause="FILE_NAME asc")
    /**
     * For collections of a basic or embeddable type use @ElementCollection.
     * 
     * To store the index value in a dedicated column, use the @javax.persistence.OrderColumn 
     * annotation on your property. This annotations describes the column name and attributes of the 
     * column keeping the index value. This column is hosted on the table containing the association 
     * foreign key. If the column name is not specified, the default is the name of the referencing property, 
     * followed by underscore, followed by ORDER (in the following example, it would be orders_ORDER).
     * 
     * A composite element mapping does not support null-able properties if you are using a <set>. 
     * There is no separate primary key column in the composite element table. Hibernate uses each 
     * column's value to identify a record when deleting objects, which is not possible with null values. 
     * You have to either use only not-null properties in a composite-element or choose a <list>, <map>, 
     * <bag> or <idbag>.
     */
    public Set<Image> getImages() {
        return images;
    }
    void setImages(Set<Image> images) {
    	this.images = images;
    }
    public void addImage(Image image) {
        images.add(image);
    }
    
    /**
     * Note that Collection represents the bag which is an unordered list and 
     * Hibernate adds null elements to your Java list if the index numbers in the 
     * database aren't continuous.
     * 
     * Bags have the most efficient performance characteristics of all the collections 
     * you can use for a bidirectional one-to-many entity association (in other words, 
     * if the collection side is inverse="true"). By default, collections in Hibernate 
     * are loaded only when they're accessed for the first time in the application. Because 
     * a bag doesn't have to maintain the index of its elements (like a list) or check for duplicate 
     * elements (like a set), you can add new elements to the bag without triggering the loading.
     * 
     * On the other hand, you can't eager-fetch two collections of bag type simultaneously.
     * 
     * You can also map a to one association through an association table. This association table 
     * described by the @JoinTable annotation contains a foreign key referencing back the entity 
     * table (through @JoinTable.joinColumns) and a a foreign key referencing the target entity table
     * (through @JoinTable.inverseJoinColumns).
     * 
     */
    @ElementCollection
    @JoinTable(name="ITEM_REVIEW",
                joinColumns=@JoinColumn(name="ITEM_ID")
    )
    @org.hibernate.annotations.IndexColumn(name="REVIEW_NUMBER",
                                            base=1
    )
    @org.hibernate.annotations.OrderBy(clause="REVIEWED_DATE desc")
    @CollectionId(columns=@Column(name="ITEM_REVIEW_ID"),
                    type=@org.hibernate.annotations.Type(type="long"),
                    generator = "native"
    )
    public Collection<Review> getReviews() {
        return reviews;
    }
    void setReviews(Collection<Review> reviews) {
    	this.reviews = reviews;
    }
    public void addReview(Review review) {
        reviews.add(review);
    }
    
    /**
     * Indexed collections (lists and maps) won't work for inverse end of a many-to-many association, because 
     * Hibernate won't initialize or maintain the index column if the collection is inverse. In other words, a many-to-many 
     * association can't be mapped with indexed collections on both sides.
     * 
     * Note mappedBy tells Hibernate to ignore changes made to the categories collection and that the other end of the association, 
     * the items collection, is the representation that should be synchronized with the database if you link instances in Java code.
     */
    @ManyToMany(mappedBy="items")
    public Set<Category> getCategories() {
        return categories;
    }
    void setCategories(Set<Category> categories) {
    	this.categories = categories;
    }
    public void addCategory(Category category) {
        categories.add(category);
    }
    
    @ManyToMany(mappedBy="boughtItems")
    /**
     * The collection is not initialized if you call size(), contains(), or isEmpty()
     */
    @org.hibernate.annotations.LazyCollection(org.hibernate.annotations.LazyCollectionOption.EXTRA)
    /**
     * One note regarding usage of entities:
     * If there exists a shared reference to the entity object , you can't delete the entity without removing the shared 
     * references first. You may get an exception if you try to commit the delete transaction, because a foreign key constraint 
     * may be violated. You have to chase the pointers. This process can get ugly.
     * 
     * In another words, avoid entity-entity mapping as much as possible.
     */
    public Set<Receipt> getReceipts() {
        return receipts;
    }
    void setReceipts(Set<Receipt> receipts) {
    	this.receipts = receipts;
    }
    public void addReceipt(Receipt receipt) {
        receipts.add(receipt);
    }
    
    /**
     * The version property will be mapped to the OPT_LOCK column, and the entity manager will use it to 
     * detect conflicting updates (preventing lost updates you might otherwise see with the last-commit wins strategy).
     */
    @Version
    @Column(name="OPT_LOCK")
    public Integer getVersion() {
        return version;
    }
    void setVersion(Integer version) {
    	this.version = version;
    }
    
    @Override
    public int hashCode() {
        int hashCodeVal = JPAConstants.HASH_CODE_INIT_VALUE;
        hashCodeVal = JPAConstants.HASH_CODE_MULTIPLY_VALUE * hashCodeVal + name.hashCode();
        hashCodeVal = JPAConstants.HASH_CODE_MULTIPLY_VALUE * hashCodeVal + description.hashCode();
        return hashCodeVal;
    }
    @Override
    public boolean equals(Object instance) {
        if(!(instance instanceof Item)) {
            return false;
        }
        
        Item item = Item.class.cast( instance );
        return item.name.equals(name) && item.description.equals(description);
    }
    @Override
    public String toString() {
        StringBuilder content = new StringBuilder();
        content.append("Item{ id: ");
        content.append(id);
        content.append(", name: ");
        content.append(name);
        content.append(", seller: ");
        content.append(seller);
        content.append(", description: ");
        content.append(description);
        content.append(", price: ");
        content.append(price);
        content.append(", shippingFee: ");
        content.append(shippingFee);
        content.append(", taxRate: ");
        content.append(taxRate);
        content.append(", lastModified: ");
        content.append(lastModified);
        content.append(", images: ");
        content.append(images);
        content.append(", reviews: ");
        content.append(reviews);
        content.append(", categories: ");
        content.append(categories);
        content.append(", receipts: ");
        content.append(receipts);
        content.append(", version: ");
        content.append(version);
        content.append(", totalIncludingTax: ");
        content.append(totalIncludingTax);
        content.append(" }");
        
        return content.toString();
    }
    
}
